﻿using UnityEngine;
using System.Collections;
using System.IO;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;

namespace GX
{
	public static class ArchiveManager
	{
		private static readonly string PathBase =
#if UNITY_ANDROID
			"jar:file://" + Application.dataPath + "!/assets/";
#elif UNITY_IPHONE
			Application.dataPath + "/Raw/";
#elif UNITY_STANDALONE_WIN || UNITY_EDITOR
			"file:///" + Path.GetDirectoryName(Application.dataPath) + "/";
#else
			string.Empty;
#endif

		private static readonly string homeBase = Path.GetFullPath(Path.Combine(Application.dataPath, "../Version/base/StandaloneWindows")).Replace('\\', '/');
		private static readonly string homePatch = Path.GetFullPath(Path.Combine(Application.dataPath, "../Version/patch/StandaloneWindows")).Replace('\\', '/');
		private static FList flist = new FList();

		static ArchiveManager()
		{
			var flistBase = FList.Load(homeBase + ".xml") ?? new FList();
			var flistPatch = FList.Load(homePatch + ".xml") ?? new FList();
			flistPatch.SetPatch(true);

			flist.AddRange(flistBase);
			flist.AddRange(flistPatch);
		}

		#region LoadAsync
		// TODO: How to return a object that can be used for Unity coroutine and can get the load
		// result from it. Maybe a custome type is need here. IEnumerator not be the best always.
		public static IEnumerator LoadAsync<T>(string path, System.Action<T> callback) where T : Object
		{
			if (callback == null)
				yield break;
			// 尝试从AssetBundle中加载
			T abs = null;
			var ablist = new List<AssetBundle>();
			try
			{
				foreach (var p in flist[path])
				{
					var url = "file:///" + (p.Key ? homePatch : homeBase) + "/" + p.Value;
					var www = new WWW(url);
					yield return www;
					var ab = www.assetBundle;
					www.Dispose();
					ablist.Add(ab);
					yield return ab.LoadAllAssetsAsync();
				}

				// AssetBundle类型直接返回
				if (typeof(T) == typeof(AssetBundle))
				{
					callback(ablist.LastOrDefault() as T);
					yield break;
				}

				if (ablist.Any())
				{
					var ab = ablist.Last();
					abs = (ab.mainAsset ?? ab.LoadAllAssets().FirstOrDefault()) as T;
					if (abs != null)
					{
						Debug.Log("<color=green>Load from AssetBundle: </color>" + path);
						callback(abs);
						yield break;
					}
				}
			}
			finally
			{
				if (ablist.Any())
				{
					foreach (var i in Enumerable.Reverse(ablist))
						i.Unload(false);
					ablist.Clear();
				}
				abs = null;
			}

			// 尝试从Resources中加载
			T res = null;
			try
			{
				var p = Path.ChangeExtension(path, null); // Resources方式的加载不需要扩展名
				var load = Resources.LoadAsync<T>(p);
				yield return load;
				res = load.asset as T;
				if (res != null)
				{
					Debug.Log("<color=yellow>Load from Resources: </color>" + p);
					callback(res);
					yield break;
				}
			}
			finally
			{
				if (res != null)
				{
					Resources.UnloadAsset(res);
					res = null;
				}
			}

			Debug.Log("<color=red>Load error: </color>" + path);
			callback(null);
		}
		#endregion

		#region Load
		public static T Load<T>(string path) where T : Object
		{
			// 尝试从AssetBundle中加载
			AssetBundle ab = null;
			foreach (var p in flist[path])
			{
				var www = new WWW("file:///" + (p.Key ? homePatch : homeBase) + "/" + p.Value);
				//yield return www;
				ab = www.assetBundle;
			}

			// AssetBundle类型直接返回
			if (typeof(T) == typeof(AssetBundle))
				return ab as T;

			if (ab != null)
			{
				var ret = ab.mainAsset as T;
				if (ret != null)
				{
					ab.Unload(false);
					Debug.Log("<color=green>Load from AssetBundle: </color>" + path);
					return ret;
				}
			}

			// 尝试从Resources中加载
			{
				var p = Path.ChangeExtension(path, null); // Resources方式的加载不需要扩展名
				var ret = Resources.Load(p, typeof(T)) as T;
				if (ret != null)
				{
					Debug.Log("<color=yellow>Load from Resources: </color>" + p);
					return ret;
				}
			}
			return null;
		}

		public static Object Load(string path)
		{
			return Load<Object>(path);
		}
		#endregion
	}

	public static partial class Extensions
	{
		public static Coroutine LoadAsync<T>(this MonoBehaviour host, string path, System.Action<T> callback) where T : UnityEngine.Object
		{
			return host.StartCoroutine(ArchiveManager.LoadAsync<T>(path, callback));
		}
	}

	#region class FList
	public class FList : IXmlSerializable, IEnumerable<FList.FileItem>
	{
		#region class FileItem
		public class FileItem : IXmlSerializable, System.IEquatable<FileItem>
		{
			public string path { get; set; }
			public string ab { get; set; }
			public long size { get; set; }
			public string[] depends { get; set; }

			public bool patch { get; set; }

			#region IXmlSerializable
			public XElement Serialize()
			{
				return new XElement("file",
				   new XAttribute("path", this.path),
				   new XAttribute("ab", this.ab),
				   new XAttribute("size", this.size),
				   from d in this.depends
				   select new XElement("file",
					   new XAttribute("ab", d)));
			}

			public void Deserialize(XElement xml)
			{
				this.path = xml.AttributeValue("path");
				this.ab = xml.AttributeValue("ab");
				this.size = long.Parse(xml.AttributeValue("size"));
				this.depends = xml.Elements("file").Select(i => i.AttributeValue("ab")).ToArray();
			}
			#endregion

			#region IEquatable<FileItem>
			public bool Equals(FileItem other)
			{
				if (other == null)
					return false;
				if (object.ReferenceEquals(this, other))
					return true;
				return this.path == other.path
					&& this.ab == other.ab
					&& this.size == other.size
					&& Enumerable.SequenceEqual(this.depends, other.depends)
					&& this.patch == other.patch;
			}
			#endregion

			public override bool Equals(object obj)
			{
				return this.Equals(obj as FileItem);
			}

			public override int GetHashCode()
			{
				return this.path.GetHashCode()
					^ this.ab.GetHashCode()
					^ this.size.GetHashCode()
					^ this.depends.Aggregate(0, (hash, value) => hash ^ value.GetHashCode())
					^ this.patch.GetHashCode();
			}
		}
		#endregion

		public string Target { get; set; }
		private readonly Dictionary<string, FileItem> pathList = new Dictionary<string, FileItem>();
		private readonly Dictionary<string, FileItem> abList = new Dictionary<string, FileItem>();

		public FList() { }
		public FList(IEnumerable<FList.FileItem> items) { this.AddRange(items); }

		public int Count { get { return pathList.Count; } }

		/// <summary>
		/// 根据逻辑路径<paramref name="path"/>得到按照依赖顺序的assetbundle文件顺序列表
		/// </summary>
		/// <param name="path">逻辑路径</param>
		/// <returns></returns>
		public IEnumerable<KeyValuePair<bool, string>> this[string path]
		{
			get
			{
				FileItem item;
				if (pathList.TryGetValue(path, out item) == false)
					return Enumerable.Empty<KeyValuePair<bool, string>>();
				return GetDepends(item).Concat(KeyValuePair.Create(item.patch, item.ab));
			}
		}

		private IEnumerable<KeyValuePair<bool, string>> GetDepends(FileItem item)
		{
			foreach (var d in item.depends)
			{
				var next = abList[d]; // 肯定是存在的，否则该flist本身的数据就是残缺的
				foreach (var i in GetDepends(next))
					yield return i;
				yield return KeyValuePair.Create(next.patch, next.ab);
			}
		}

		public void Add(FileItem item)
		{
			this.pathList[item.path] = item;
			this.abList[item.ab] = item;
		}

		public void AddRange(IEnumerable<FileItem> items)
		{
			foreach (var i in items)
				this.Add(i);
		}

		public void Clear()
		{
			this.pathList.Clear();
			this.abList.Clear();
		}

		/// <summary>
		/// 设置所有子项的<c>patch</c>属性
		/// </summary>
		/// <param name="value"></param>
		public void SetPatch(bool value)
		{
			foreach (var i in this)
				i.patch = value;
		}

		public static FList Load(string path)
		{
			try
			{
				var flist = new FList();
				flist.Deserialize(XDocument.Load(path).Root);
				return flist;
			}
			catch { return null; }
		}

		public void Save(string path)
		{
			new XDocument(this.Serialize()).Save(path, new System.Xml.XmlWriterSettings()
			{
				Indent = true,
				IndentChars = "\t",
				Encoding = System.Text.Encoding.UTF8,
			});
		}

		#region IXmlSerializable
		public XElement Serialize()
		{
			return new XElement("files",
				new XAttribute("target", Target),
				from i in pathList.Values.OrderBy(i => i.path, new GX.NaturalPathComparer()) // flist采用自然排序
				select i.Serialize());
		}

		public void Deserialize(XElement xml)
		{
			this.Target = xml.AttributeValue("target");
			this.Clear();
			foreach (var i in xml.Elements("file"))
			{
				var item = new FileItem();
				item.Deserialize(i);
				this.Add(item);
			}
		}
		#endregion

		#region IEnumerable<FList.FileItem>
		public IEnumerator<FileItem> GetEnumerator()
		{
			foreach (var i in this.pathList.Values)
				yield return i;
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return this.GetEnumerator();
		}
		#endregion
	}
	#endregion
}
